Skip to content

[fix] Fixed double encoding of device model labels in dashboard #651 #666

Merged
nemesifier merged 2 commits into
openwisp:masterfrom
BHARATH0153:fix-dashboard-double-encoding
May 12, 2026
Merged

[fix] Fixed double encoding of device model labels in dashboard #651 #666
nemesifier merged 2 commits into
openwisp:masterfrom
BHARATH0153:fix-dashboard-double-encoding

Conversation

@BHARATH0153
Copy link
Copy Markdown
Contributor

@BHARATH0153 BHARATH0153 commented May 6, 2026

Checklist

I have read the OpenWISP Contributing Guidelines.

I have manually tested the changes proposed in this pull request.

I have written new test cases for new code and/or updated existing tests for changes to existing code.

I have updated the documentation.

Reference to Existing Issue

Fixes #651

Description of Changes

Hoisted filters.append Moved the append logic above the if/else block to remove the duplication and keep the code DRY.

Server-side URL Encoding Wrapped the label in quote(label, safe="") from urllib.parse before appending it to filters. This ensures the JS click handler receives a fully URL-encoded string (e.g., %26 instead of &), preventing the URL parsing issue in the browser.

Local validation:

black openwisp_utils/admin_theme/dashboard.py tests/test_project/tests/test_dashboard.py
python runtests.py test_project.tests.test_dashboard.TestAdminDashboard.test_filter_not_double_encoded -v 2
python runtests.py test_project.tests.test_dashboard.TestAdminDashboard.test_index_content -v 2

Screenshot

f1 before fix after fix after fix

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The group_by loop in the admin dashboard now URL-quotes and appends the DB-derived label to chart filters unconditionally, then uses i18n mappings when present or HTML-escapes the DB label for labels. A new test, test_filter_not_double_encoded, verifies filters contain the URL-encoded value while query_params["labels"] contains the HTML-escaped form and constructed filter URLs are not double-encoded.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AdminUI
  participant Dashboard
  participant DB
  participant Template
  User->>AdminUI: request dashboard / click chart
  AdminUI->>Dashboard: get_dashboard_context()
  Dashboard->>DB: query group-by labels
  DB-->>Dashboard: raw label (e.g., "Dongwon T&I")
  Dashboard->>Dashboard: filters.append(URL-quote label)
  alt labels_i18n exists
    Dashboard->>Dashboard: labels.append(i18n label)
  else
    Dashboard->>Dashboard: labels.append(HTML-escaped label)
  end
  Dashboard->>Template: context with filters and labels
  Template->>User: rendered page / generated filter URL uses quoted value
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description check ✅ Passed The description covers all essential sections: checklist items, reference to issue #651, clear explanation of changes (hoisting filters.append and server-side URL encoding), local validation steps, and before/after screenshots.
Linked Issues check ✅ Passed The PR directly addresses issue #651 by implementing server-side URL encoding (urllib.parse.quote) and hoisting the filters.append logic to prevent double-encoding of special characters in dashboard filter URLs.
Out of Scope Changes check ✅ Passed All changes are focused on fixing the double-encoding issue in dashboard filters; modifications to dashboard.py and addition of test_filter_not_double_encoded are directly related to issue #651 requirements.
Bug Fixes ✅ Passed Root cause fixed by using quote() before escaping. Regression test reproduces bug scenario with ampersand, validates correct encoding, would fail without fix. Test is deterministic.
Title check ✅ Passed The title follows the required [type] format with '[fix]' prefix and clearly describes the specific issue fixed: double encoding of device model labels in the dashboard, with the linked issue number #651.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kilo-code-bot
Copy link
Copy Markdown

kilo-code-bot Bot commented May 6, 2026

Code Review Summary

Status: 1 CRITICAL Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 1
WARNING 0
SUGGESTION 0
Issue Details (click to expand)

CRITICAL

File Line Issue
openwisp_utils/admin_theme/dashboard.py 221 Fix missing - uses html.escape() causing double-encoding
tests/test_project/tests/test_dashboard.py N/A Regression test missing
Explanation

The PR claims to fix issue #651 (double encoding of device model labels), but the fix is missing from the code.

Current code (line 221):

label = html.escape(label)

This causes double-encoding when device model names contain & - they become & in the label and then %26amp%3B when URL-encoded by the browser.

Required to fix #651:

  1. Add filters.append(label) before escaping (to store raw value for URLs)
  2. Change html.escape(label) to mark_safe(conditional_escape(label))
  3. Restore the test_filter_not_double_encoded regression test
Files Reviewed
  • openwisp_utils/admin_theme/dashboard.py - CRITICAL: Fix missing, double-encoding issue persists
  • tests/test_project/tests/test_dashboard.py - CRITICAL: Regression test missing
  • Bot utility files - Various updates

Reviewed by kimi-k2.5 · 563,346 tokens

@openwisp-companion
Copy link
Copy Markdown

CI Failures: Formatting and Commit Message

Hello @BHARATH0153,
(Analysis for commit 0315e1d)

  1. Code Style Failures:
  • The CI failed due to code formatting issues (missing newlines, incorrect import sorting, and blank lines with whitespace). Please run openwisp-qa-format to automatically fix these issues.
  • The flake8 check also reported W292 no newline at end of file and W293 blank line contains whitespace, which openwisp-qa-format should resolve.
  1. Commit Message Failure:
  • The commit message does not adhere to the OpenWISP commit convention.

Correct format example:

[feature] Add new feature #123

This adds a new feature.

Fixes #123

@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch 3 times, most recently from 26dc614 to b9ebc2e Compare May 6, 2026 14:13
@coveralls
Copy link
Copy Markdown

coveralls commented May 6, 2026

Coverage Status

coverage: 97.458% (-0.06%) from 97.513% — BHARATH0153:fix-dashboard-double-encoding into openwisp:master

@BHARATH0153
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@BHARATH0153
Copy link
Copy Markdown
Contributor Author

BHARATH0153 commented May 6, 2026

@nemesifier please review when you are free
locally reproduced and fixed as above mentioned the proofs #666 (comment) (screenshot) thanks!

@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch from 80c0e65 to 540fe31 Compare May 7, 2026 05:53
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 7, 2026
@openwisp-companion
Copy link
Copy Markdown

CI Failures Summary

Hello @BHARATH0153,
(Analysis for commit 540fe31)

  1. Code Style/QA: The CI failed due to code style violations (Black and Flake8).
  • Fix: Run openwisp-qa-format to automatically fix these issues.
  1. Commit Message: The commit message does not adhere to the required format.
  • Fix: Ensure your commit message follows the OpenWISP convention. For example:
[feature] Add new functionality #123

This commit introduces a new feature that allows users to do X.
It includes changes to the following modules: ...

Fixes #123

BHARATH0153 added a commit to BHARATH0153/openwisp-utils that referenced this pull request May 7, 2026
Fixed issue where special characters (e.g. &) in project names were
being double-encoded when used as dashboard chart filters.

Closes openwisp#666
@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch 2 times, most recently from e09da99 to eba06ed Compare May 7, 2026 06:04
BHARATH0153 added a commit to BHARATH0153/openwisp-utils that referenced this pull request May 7, 2026
…isp#666

Fixed special characters like & in project names being double-encoded
when used as dashboard chart filters.
Fixes openwisp#666
BHARATH0153 added a commit to BHARATH0153/openwisp-utils that referenced this pull request May 7, 2026
…isp#666

Fixed special characters like & in project names being double-encoded
when used as dashboard chart filters.

Fixes openwisp#666
@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch from eba06ed to 2326933 Compare May 7, 2026 06:06
BHARATH0153 added a commit to BHARATH0153/openwisp-utils that referenced this pull request May 7, 2026
Fixed special characters like & in project names being double-encoded
when used as dashboard chart filters.

Fixes openwisp#666
@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch from 2326933 to 19d03f8 Compare May 7, 2026 06:09
@BHARATH0153
Copy link
Copy Markdown
Contributor Author

BHARATH0153 commented May 7, 2026

@coderabbitai check the written new test in test_dashboard.py and review the changes is ready to merge

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the full test_dashboard.py file
fd test_dashboard.py --exec cat {}

Length of output: 13389


🏁 Script executed:

#!/bin/bash
# Read dashboard.py to understand filters structure
fd dashboard.py openwisp_utils --exec cat {}

Length of output: 10706


@BHARATH0153 Here is my analysis of test_filter_not_double_encoded:


✅ What the Test Does Well

  1. Correctly targets the fix — it asserts the raw "Dongwon T&I" appears in filters and the HTML-escaped "Dongwon T&I" is absent from filters but present in labels. This maps cleanly onto the logic in get_dashboard_context:

    # dashboard.py – group_by else-branch
    filters.append(label)          # raw value stored here ✅
    label = html.escape(label)     # escaped only for display
    labels.append(label)
  2. Chart lookup by name — iterating context["dashboard_charts"].values() and matching on chart.get("name") is more robust than the index-based lookup used in the older test_get_dashboard_context_html_escape.

  3. DB isolationDjangoTestCase wraps every test in a transaction that is rolled back, so the created Project/Operator objects don't leak into other tests.


⚠️ Suggestions for Improvement

1. The double-encoded form is never explicitly asserted

The test is named test_filter_not_double_encoded, but it never checks for the actual double-encoded string. Adding this assertion makes the intent explicit and guards against a regression where both raw and double-encoded forms end up in filters:

# assert double-encoded form (the original bug) is absent from filters
self.assertNotIn("Dongwon T&I", filters)

2. The labels_i18n branch is not exercised

The fix correctly stores raw labels in filters in both branches of the group_by loop, but only the else branch (no predefined labels) is tested. Consider adding a subTest for a chart that has labels_i18n to ensure the labels_i18n path also stores unescaped values in filters.

3. Use subTest for clearer failure messages

The rest of the class uses subTest heavily. Wrapping the three assertIn/assertNotIn calls would give more precise failure output:

with self.subTest("raw label in filters"):
    self.assertIn("Dongwon T&I", filters)
with self.subTest("single-encoded form not in filters"):
    self.assertNotIn("Dongwon T&I", filters)
with self.subTest("double-encoded form not in filters"):
    self.assertNotIn("Dongwon T&I", filters)
with self.subTest("escaped label in query_params labels"):
    self.assertIn("Dongwon T&I", labels)

4. Minor: get("filters", []) gives a silent empty list

If filters is unexpectedly absent from target_chart, self.assertIn("Dongwon T&I", []) will fail but the error message won't mention what key was missing. You could make it more explicit:

self.assertIn("filters", target_chart, "chart is missing 'filters' key")
filters = target_chart["filters"]

Summary

The test is functionally sound and validates the core fix correctly. The two most actionable improvements are explicitly checking for the double-encoded form (point 1) and adding a test for the labels_i18n branch (point 2).

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me pull up the actual code to review it thoroughly.


🏁 Script executed:

#!/bin/bash
echo "=== dashboard.py ==="
cat openwisp_utils/admin_theme/dashboard.py

echo ""
echo "=== test_dashboard.py ==="
cat tests/test_project/tests/test_dashboard.py

Length of output: 24143


@BHARATH0153 I've reviewed both files thoroughly. Here's my assessment:


✅ Fix in openwisp_utils/admin_theme/dashboard.py

The root cause was that in the else branch of the group_by loop, filters.append(label) was being called after html.escape() had already been applied to the label. This meant the URL filter received "Dongwon T&I" instead of the raw "Dongwon T&I", causing double-encoding in the generated URL.

The fix correctly reorders the operations:

else:
    filters.append(label)        # ← raw label stored first (for URL)
    label = html.escape(label)   # ← then escape for XSS-safe display
labels.append(label)             # ← escaped label for UI

This is minimal, targeted, and correct. The labels_i18n branch was already safe since it appended the raw qs_key before substituting the pre-escaped i18n label.


✅ New test test_filter_not_double_encoded

The test correctly covers the three key assertions:

Assertion Purpose
assertIn("Dongwon T&I", filters) Raw label is used for URL filter
assertNotIn("Dongwon T&I", filters) HTML-encoded string is NOT in filter
assertIn("Dongwon T&I", labels) Escaped label is shown in UI (XSS safe)

Minor suggestions (non-blocking)

  1. Failure message on assertIsNotNone: If the chart name ever changes, the test will fail with a cryptic None is not None message. Consider:

    self.assertIsNotNone(
        target_chart,
        "Chart 'Operator Project Distribution' not found in dashboard_charts"
    )
  2. Chart name coupling: The test depends on the hardcoded string "Operator Project Distribution". This is acceptable since it mirrors how the chart is registered, but worth noting as a maintenance point.


Overall verdict

The fix is correct, the test covers the bug precisely, XSS protection is preserved (test_get_dashboard_context_html_escape still passes), and no regressions are introduced. The PR is ready to merge pending maintainer sign-off. 🎉

@BHARATH0153
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/test_project/tests/test_dashboard.py (1)

275-316: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Extend test_get_dashboard_context_html_escape to assert filters holds the raw (unescaped) XSS payload.

The new test correctly validates the double-encoding fix. However, now that filters stores raw DB values (not HTML-escaped ones), test_get_dashboard_context_html_escape only tests that labels are escaped—it doesn't assert that filters contains the raw malicious value. This leaves a gap: if a future change accidentally re-escapes filters, only the URL test would catch it, not the XSS test.

Consider adding to test_get_dashboard_context_html_escape:

✅ Proposed addition to test_get_dashboard_context_html_escape
     # ensure DB value is escaped
     self.assertEqual(
         context["dashboard_charts"][0]["query_params"]["labels"][0],
         "<script>alert(1)</script>",
     )
+    # ensure filters keep the raw value (not double-escaped) for correct URL generation
+    self.assertIn(
+        "<script>alert(1)</script>",
+        context["dashboard_charts"][0].get("filters", []),
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_project/tests/test_dashboard.py` around lines 275 - 316, Extend
test_get_dashboard_context_html_escape to also assert that filters contain the
raw DB value (unescaped) so we don't regress to double-encoding: after calling
get_dashboard_context(mocked_request), fetch filters from
context["dashboard_charts"][0]["filters"] (the same chart used for labels) and
add assertions that the raw string "<script>alert(1)</script>" is present in
filters and that its escaped form "&lt;script&gt;alert(1)&lt;/script&gt;" is
not; keep the existing label assertions intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@tests/test_project/tests/test_dashboard.py`:
- Around line 275-316: Extend test_get_dashboard_context_html_escape to also
assert that filters contain the raw DB value (unescaped) so we don't regress to
double-encoding: after calling get_dashboard_context(mocked_request), fetch
filters from context["dashboard_charts"][0]["filters"] (the same chart used for
labels) and add assertions that the raw string "<script>alert(1)</script>" is
present in filters and that its escaped form
"&lt;script&gt;alert(1)&lt;/script&gt;" is not; keep the existing label
assertions intact.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b7c3a9f7-2e15-4168-998f-7fcd29bd8d55

📥 Commits

Reviewing files that changed from the base of the PR and between 540fe31 and 2108b58.

📒 Files selected for processing (2)
  • openwisp_utils/admin_theme/dashboard.py
  • tests/test_project/tests/test_dashboard.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{py,html,txt}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • tests/test_project/tests/test_dashboard.py
  • openwisp_utils/admin_theme/dashboard.py
🔇 Additional comments (1)
openwisp_utils/admin_theme/dashboard.py (1)

203-231: ⚡ Quick win

No XSS vulnerability exists. The filters array is correctly stored with raw (unescaped) DB values, as confirmed by the test test_filter_not_double_encoded, which asserts filters contain unescaped values like "Dongwon T&I".

The template renders dashboard_charts as a JavaScript object literal inside a <script> tag using {{ dashboard_charts | safe }}, meaning filters values are embedded as strings within JSON, not parsed as HTML. In ow-dashboard.js, filters are only used to concatenate URL query parameters (path += filters[i]), where they are treated as URL-safe strings, not HTML.

The change is correct and safe.

			> Likely an incorrect or invalid review comment.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 7, 2026
@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch from 895fec7 to f5ba670 Compare May 10, 2026 05:08
@BHARATH0153
Copy link
Copy Markdown
Contributor Author

@coderabbitai is it ready to merge now after maintainer review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Tip

For best results, initiate chat on the files or code changes.

@BHARATH0153 Based on the full comment chain and the information available to me, here is an honest summary of where things stand:

From a technical standpoint — Yes, the PR is ready. ✅

Check Status
Bug fix in dashboard.py (raw label appended to filters before html.escape) ✅ Correct
XSS protection preserved (test_get_dashboard_context_html_escape passes) ✅ Confirmed
New test test_filter_not_double_encoded validates the fix ✅ Passes
CI / coverage (~97.458%) ✅ Acceptable

From a process standpoint — it still needs maintainer sign-off. ⏳

Per OpenWISP's contributing guidelines, a PR will be merged only once the CI build completes successfully and after project maintainers have reviewed and tested it. Based on the comment chain, @nemesifier has not yet posted a review or approval on this PR.

The decision to merge belongs exclusively to the project maintainers. I'd suggest pinging @nemesifier directly one more time with a polite nudge, noting:

  1. The fix is a minimal, targeted one-line change.
  2. All tests pass, including the pre-existing HTML escape test.
  3. The fix has been through multiple automated review rounds without any blocking issues found.

There's nothing more to change on your end — the ball is in the maintainers' court. 🎉

@BHARATH0153
Copy link
Copy Markdown
Contributor Author

@nemesifier oooooo
changes are addressed and tests are passing

@BHARATH0153 BHARATH0153 requested a review from nemesifier May 10, 2026 05:35
@BHARATH0153 BHARATH0153 force-pushed the fix-dashboard-double-encoding branch from f5ba670 to c7f8dd8 Compare May 10, 2026 10:46
@openwisp-companion
Copy link
Copy Markdown

The CI is failing due to transient infrastructure issues (not related to your code). I have restarted the failed jobs automatically (1/3).

@BHARATH0153
Copy link
Copy Markdown
Contributor Author

BHARATH0153 commented May 10, 2026

@coderabbitai check overall improvement and review and is it ready to merge and check fulfillment of @nemesifier changes or comment

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get PR reviews
echo "=== PR REVIEWS ==="
gh pr view 666 --repo openwisp/openwisp-utils --json reviews --jq '.reviews[] | {author: .author.login, state: .state, body: .body, submittedAt: .submittedAt}'

echo ""
echo "=== PR COMMENTS (issue-level) ==="
gh pr view 666 --repo openwisp/openwisp-utils --json comments --jq '.comments[] | {author: .author.login, body: .body[0:300], createdAt: .createdAt}'

echo ""
echo "=== PR REVIEW COMMENTS (inline) ==="
gh api repos/openwisp/openwisp-utils/pulls/666/comments --jq '.[] | {author: .user.login, body: .body[0:300], path: .path, line: .line, created_at: .created_at}'

Length of output: 17420


@BHARATH0153 @nemesifier has posted a CHANGES_REQUESTED review (2026-05-09) — the PR is not yet ready to merge. There are two concrete issues that must be addressed:


🚫 Blocking Issues from @nemesifier

1. Refactor: Hoist filters.append(label) above the if/else

The filters.append(label) line is now duplicated in both branches of the if/else. It should be moved above the branch:

Current (after your fix):

if labels_i18n and qs_key in labels_i18n:
    filters.append(label)          # duplicated
    label = labels_i18n[qs_key]
else:
    filters.append(label)          # duplicated
    # HTML escape labels coming from values in the DB
    # to avoid possible XSS attacks caused by
    # malicious DB values set by users
    label = html.escape(label)

Required change:

filters.append(label)              # hoisted — one line, both branches covered
if labels_i18n and qs_key in labels_i18n:
    label = labels_i18n[qs_key]
else:
    # HTML escape labels coming from values in the DB
    # to avoid possible XSS attacks caused by
    # malicious DB values set by users
    label = html.escape(label)

2. Critical: The JavaScript never URL-encodes the filter value

This is the more important issue. In ow-dashboard.js, the filter is appended to the URL path as-is:

if (filters && typeof filters[i] !== "undefined") {
    path += filters[i];   // ← no encodeURIComponent!
}

With the raw string "Dongwon T&I DW02-412H (128M)", the browser will see ?model__exact=Dongwon T&I DW02-412H (128M), and the & breaks it into two query parameters — so the filter only gets model__exact=Dongwon T, which is wrong.

The expected URL from issue #651 is ?model__exact=Dongwon%20T%26I%20DW02-412H%20%28128M%29, which requires encodeURIComponent.

Fix option A — in JavaScript:

if (filters && typeof filters[i] !== "undefined") {
    path += encodeURIComponent(filters[i]);
}

Fix option B — pre-encode on the server (in dashboard.py):

from urllib.parse import quote
filters.append(quote(label, safe=''))

Option A (JS-side encodeURIComponent) is cleaner and more idiomatic.


3. Test coverage must be extended

The existing test only checks server-side context data. @nemesifier asks for a test that validates the actual URL string produced (either via Selenium, or a small unit test simulating the JS string concatenation logic).


Summary

Issue Status
filters.append(label) hoisted above if/else ❌ Not done
encodeURIComponent in JS (or server-side pre-encoding) ❌ Not done
Test covering the final URL output ❌ Not done
CI passing ⚠️ Transient failures (auto-restarted)

Please address these two code changes and extend the test before requesting another review from @nemesifier.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== PR Reviews ==="
gh pr view 666 --repo openwisp/openwisp-utils --json reviews,reviewRequests,state,mergeable

echo ""
echo "=== PR Comments (last 20) ==="
gh pr view 666 --repo openwisp/openwisp-utils --json comments --jq '.comments[-20:] | .[] | {author: .author.login, body: .body[:300]}'

echo ""
echo "=== PR Review Comments (inline) ==="
gh api repos/openwisp/openwisp-utils/pulls/666/comments --jq '.[] | {author: .user.login, body: .body[:300], path: .path, line: .line}'

echo ""
echo "=== Current diff / latest changes ==="
gh pr diff 666 --repo openwisp/openwisp-utils

Length of output: 17811


@BHARATH0153 — I've now read @nemesifier's CHANGES_REQUESTED review (posted 2026-05-09) and cross-checked it against your latest diff. Here is the full status:


@nemesifier raised two blocking issues

1. ✅ Addressed — Hoist filters.append() above the if/else

@nemesifier noted that filters.append(label) was duplicated in both branches and that the comment about html.escape no longer described the line directly below it.

Your latest diff fixes this cleanly:

filters.append(quote(label, safe=""))   # ← hoisted above if/else ✅
if labels_i18n and qs_key in labels_i18n:
    label = labels_i18n[qs_key]
else:
    # HTML escape labels coming from values in the DB …
    label = html.escape(label)

Both duplication and comment-misalignment are resolved.


2. ✅ Addressed — URL encoding before appending to filters

@nemesifier identified that the JS click handler simply does path += filters[i] with no encodeURIComponent, so a raw Dongwon T&I would break the URL at &. The fix needed either JS-side encoding or server-side pre-encoding.

Your solution uses urllib.parse.quote(label, safe="") on the server side, so filters now receives "Dongwon%20T%26I" instead of the raw string. The JS concatenation then produces a correctly formed URL. ✅


Updated test coverage

The new test_filter_not_double_encoded reflects the changes:

Assertion Purpose
assertIn("Dongwon%20T%26I", filters) Percent-encoded value is in filters ✅
assertNotIn("Dongwon T&I", filters) Raw/unencoded value is absent ✅
assertIn("Dongwon T&amp;I", labels) Display label is HTML-escaped ✅
assertIn("%26", final_url) Final URL has & properly encoded ✅
assertNotIn("&I", final_url.split("=",1)[-1]) No broken query param split ✅

Overall status

Check Status
@nemesifier point 1 (hoist filters.append) ✅ Addressed
@nemesifier point 2 (URL encoding for JS) ✅ Addressed with urllib.parse.quote
XSS protection preserved html.escape still applied to labels
Test covers the fix end-to-end
CI ⏳ Awaiting green (transient failure was auto-restarted)

The PR is technically ready. @nemesifier's review is still marked as CHANGES_REQUESTED because they haven't re-reviewed the latest commits yet. Please ping them directly to let them know both of their concerns have been addressed and the PR is ready for a second look.

@BHARATH0153
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_project/tests/test_dashboard.py`:
- Around line 321-323: Replace the brittle substring checks around final_url
(constructed from target_chart["target_link"] + filters[filter_index]) with
proper URL parsing: use urllib.parse.urlparse on final_url, extract the query,
use urllib.parse.parse_qs (or parse_qsl) to find the specific query parameter
value you care about, then use urllib.parse.unquote or unquote_plus to assert
the decoded parameter does not contain the raw "&I" sequence and assert the raw
encoded query contains "%26" where expected; update the assertions that
reference final_url, target_chart["target_link"], and filters[filter_index] to
use these parsed/decoded values instead of split/substring checks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 693e3506-9c3c-4101-b75d-607ab3e1ca95

📥 Commits

Reviewing files that changed from the base of the PR and between 571e30f and c7f8dd8.

📒 Files selected for processing (2)
  • openwisp_utils/admin_theme/dashboard.py
  • tests/test_project/tests/test_dashboard.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{py,html,txt}

📄 CodeRabbit inference engine (Custom checks)

For Django pull requests, ensure all user-facing strings are marked as translatable using the Django i18n framework

Files:

  • tests/test_project/tests/test_dashboard.py
  • openwisp_utils/admin_theme/dashboard.py
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Run openwisp-qa-format to reformat Python and adjacent files in place before considering a change complete
Prioritize comments and docstrings that explain why the code is shaped this way (constraints, trade-offs, references to issues) rather than translating code into prose

Files:

  • tests/test_project/tests/test_dashboard.py
  • openwisp_utils/admin_theme/dashboard.py
**/*.{py,css,js,txt}

📄 CodeRabbit inference engine (AGENTS.md)

Run ./run-qa-checks which executes openwisp-qa-check with project linter configuration for CSS, JS, and migration paths, and treat any failure as blocking

Files:

  • tests/test_project/tests/test_dashboard.py
  • openwisp_utils/admin_theme/dashboard.py
**/*test*.py

📄 CodeRabbit inference engine (AGENTS.md)

When fixing a bug, write a failing test first that reproduces the bug before implementing the fix, then confirm the test passes after the fix

Files:

  • tests/test_project/tests/test_dashboard.py
**/tests/test_*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/tests/test_*.py: For subprocess-based tests, prefer adding in-process unit tests that import the module and call its public methods directly instead of relying solely on subprocess CLI invocation, to ensure coverage instrumentation captures the code
Before declaring a test suite complete, run pytest with --cov against the file under test and confirm the coverage report reflects what you expect, checking for subprocess-only tests that report ~0% coverage with most of the file under 'Missing'

Files:

  • tests/test_project/tests/test_dashboard.py
🔇 Additional comments (3)
openwisp_utils/admin_theme/dashboard.py (2)

213-220: Good separation of URL value vs display label.

This correctly keeps filters URL-safe while preserving HTML-escaped labels for rendering, which addresses the double-encoding/XSS split cleanly.


213-213: JavaScript correctly handles pre-encoded filters without re-encoding.

The frontend contract matches the server-side change. At line 119 of ow-dashboard.js, pre-encoded filters are appended directly to the path without encodeURIComponent(). The fallback at line 121 only encodes the label when a filter is unavailable, preventing any double-encoding. The implementation is correct.

tests/test_project/tests/test_dashboard.py (1)

298-324: Nice regression coverage for encoding vs escaping.

This test captures the intended behavior split (filters URL-encoded, labels HTML-escaped) and protects against the original bug.

Comment thread tests/test_project/tests/test_dashboard.py
coderabbitai[bot]
coderabbitai Bot previously approved these changes May 10, 2026
@openwisp-companion
Copy link
Copy Markdown

Code Style and Test Failures

Hello @BHARATH0153,
(Analysis for commit fbc5211)

There are multiple failures in the CI log:

  1. Black and Flake8 Failures: The Black check failed! and Flake8 check failed! errors indicate issues with code formatting and style. Specifically, there's an IndentationError: unexpected indent in tests/test_project/tests/test_dashboard.py at line 323.
  • Fix: Run openwisp-qa-format to automatically fix most style issues. For the IndentationError, manually correct the indentation in tests/test_project/tests/test_dashboard.py.
  1. ReStructuredText Check Failure: The ReStructuredText check failed! error, along with ParserSyntaxError: Syntax Error @ 324:9, suggests an issue with the reStructuredText formatting in tests/test_project/tests/test_dashboard.py.
  • Fix: This is likely related to the indentation error. After fixing the indentation, the reStructuredText formatting should also be corrected.
  1. Test Failure (ImportError: Failed to import test module: test_project.tests.test_dashboard): This error, stemming from the IndentationError, prevents the test module from being imported correctly.
  • Fix: Resolve the indentation error mentioned above. This should allow the tests to import and run properly.

Please address these issues and push a new commit.

…p#651

Fixed the double-encoding issue in admin dashboard charts where device
models with special characters (like &) generated malformed URLs.

Updated get_dashboard_context to store raw strings in the filters array
for correct JavaScript URL generation, and applied mark_safe with
conditional_escape exclusively to UI labels to maintain XSS security.

Fixes openwisp#651
@BHARATH0153
Copy link
Copy Markdown
Contributor Author

@coderabbitai now is it ready for merge check new commit

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Latest PR state ==="
gh pr view 666 --repo openwisp/openwisp-utils --json state,mergeable,reviews,statusCheckRollup,headRefOid \
  --jq '{state, mergeable, headCommit: .headRefOid, reviews: [.reviews[] | {author: .author.login, state: .state, submittedAt: .submittedAt}], ciStatus: [.statusCheckRollup[] | {name: .name, status: .status, conclusion: .conclusion}]}'

echo ""
echo "=== Latest diff ==="
gh pr diff 666 --repo openwisp/openwisp-utils

echo ""
echo "=== Inline review comments (unresolved) ==="
gh api repos/openwisp/openwisp-utils/pulls/666/comments \
  --jq '.[] | {author: .user.login, body: .body[:400], path: .path, line: .line, resolved: (if .position == null then "possibly resolved" else "open" end)}'

Length of output: 6229


@BHARATH0153 I've checked the latest commit (7161e77). Here's the full picture:


✅ CI — Fully Green

All 14 Python/Django matrix combinations completed with SUCCESS:

Python Django 4.2 Django 5.0 Django 5.1 Django 5.2
3.10
3.11
3.12
3.13

Coveralls also reports SUCCESS. No failures anywhere.


✅ Code — Both of @nemesifier's Blocking Issues Addressed

1. filters.append() hoisted above if/else

filters.append(quote(label, safe=""))   # hoisted — no duplication
if labels_i18n and qs_key in labels_i18n:
    label = labels_i18n[qs_key]
else:
    label = html.escape(label)          # XSS protection still intact

2. URL encoding applied via urllib.parse.quote
"Dongwon T&I""Dongwon%20T%26I" in filters, so JS path concatenation produces a valid URL.


✅ Test — Core Assertions Are Solid

Assertion Purpose
assertIn("Dongwon%20T%26I", filters) URL-encoded value in filters ✅
assertNotIn("Dongwon T&I", filters) Raw unencoded value absent ✅
assertIn("Dongwon T&amp;I", labels) HTML-escaped label for display ✅
assertIn("%26", final_url) Final URL correctly encoded ✅
assertNotIn("&I", final_url.split("=",1)[-1]) No broken query param split ✅

⏳ Only Remaining Blocker — @nemesifier Re-review

The PR is MERGEABLE with no conflicts, but @nemesifier's CHANGES_REQUESTED review (2026-05-09) has not been updated — they haven't re-reviewed the latest commits yet.

There is one unresolved nitpick from me about using urllib.parse.parse_qs for stronger URL assertions in the test, but it is explicitly marked as 🔵 Trivial / non-blocking.

The ball is in @nemesifier's court. Everything requested has been implemented correctly. Please ping them one more time to let them know the PR is ready for a second look.

@nemesifier nemesifier changed the title Fix double encoding of device model labels in dashboard #651 [fix] Fixed double encoding of device model labels in dashboard #651 May 12, 2026
@github-project-automation github-project-automation Bot moved this from To do (general) to In progress in OpenWISP Contributor's Board May 12, 2026
@nemesifier nemesifier merged commit 1552923 into openwisp:master May 12, 2026
33 checks passed
@github-project-automation github-project-automation Bot moved this from In progress to Done in OpenWISP Contributor's Board May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Development

Successfully merging this pull request may close these issues.

[bug] Double Encoding in Admin Dashboard Device Models segment

3 participants